// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Author: Markus Gutschke

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "cfg.h"
#include "check.h"
#include "json.h"
#include "misc.h"
#include "stringprintf.h"
#include "utf8.h"


struct Cfg {
  struct JSON json;
  int         isDaemon;
  const char  *fname;
  time_t      mtime;
  FILE        *fp;
};

struct Cfg *newCfg(const char *fname, int isDaemon) {
  struct Cfg *cfg;
  check(cfg = malloc(sizeof(struct Cfg)));
  initCfg(cfg, fname, isDaemon);
  return cfg;
}

void initCfg(struct Cfg *cfg, const char *fname, int isDaemon) {
  memset(cfg, 0, sizeof(struct Cfg));
  initJSON(&cfg->json, "{}");
  check(cfg->fname = strdup(fname));
  cfg->isDaemon = isDaemon;
}

void destroyCfg(struct Cfg *cfg) {
  if (cfg) {
    check(!cfg->fp);
    destroyJSON(&cfg->json);
    free((void *)cfg->fname);
  }
}

void deleteCfg(struct Cfg *cfg) {
  destroyCfg(cfg);
  free(cfg);
}

static void lockCfg(struct Cfg *cfg) {
  check(!cfg->fp);

  // Not all file systems support O_EXCL for the atomic creation of lock files.
  // We therefore create a uniquely named file in the first step, and then
  // try to link it to the lock file.
  char *tmpFname = stringPrintf(NULL, "%s.%d.%s", cfg->fname,
                                (int)pid(), hostname());
  int fd;
  for (;;) {
    fd = TEMP_FAILURE_RETRY(open(tmpFname, O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,
                                 0600));
    if (fd < 0 && errno == EEXIST) {
      unlink(tmpFname);
    } else {
      break;
    }
  }
  check (cfg->fp = fdopen(fd, "w"));

  // Write our pid and hostname into the configuration file. This information
  // is always found at the very top of the file and can be used to detect
  // stale lock files.
  fprintf(cfg->fp, "{\"lock\":\"%d %s\"}", (int)pid(), hostname());
  fflush(cfg->fp);

  // Attempt to link to the lock file
  char *lockFname = stringPrintf(NULL, "%s.lock", cfg->fname);
  for (;;) {
    int rc = link(tmpFname, lockFname);
    if (!rc) {
      // If there was no lock contention, we just successfully acquired the
      // lock
      break;
    }
    struct stat sb;
    if (!lstat(tmpFname, &sb) &&
        !S_ISREG(sb.st_mode) &&
        sb.st_nlink == 2) {
      // On some file systems, the call to link(2) could fail, but still
      // successfully establish a link.
      break;
    }

    // There already exists a lock file. We should check whether it is valid.
    // If we can't open it, then we have to assume that it is valid, but we
    // just don't have the required permissions to check. There is not much
    // we can do other than wait for the lock to be released.
    int fd = TEMP_FAILURE_RETRY(open(lockFname, O_RDONLY));
    if (fd < 0) {
    retry:
      poll(NULL, 0, 200 + rand() % 800);
      continue;
    }

    // Read the beginning of the lock file. If it is well-formed, it contains
    // information about the lock-file holder. If it is not well-formed, maybe
    // data hasn't properly propagated (network filesystem?). We just have to
    // wait a little bit.
    char buf[4097];
    rc = TEMP_FAILURE_RETRY(read(fd, buf, sizeof(buf) - 1));
    TEMP_FAILURE_RETRY(close(fd));
    if (rc < 0) {
      goto retry;
    }
    buf[rc] = '\000';
    char *ptr = strstr(buf, "\"lock\":");
    if (!ptr) {
      // Couldn't find a "lock:" entry
      goto retry;
    }
    ptr += 7;
    while (*ptr > '\000' && *ptr <= ' ') {
      ++ptr;
    }
    if (*ptr == '"') {
      ++ptr;
    } else {
      // Couldn't find the first '"'
      goto retry;
    }
    char *end = ptr;
    char *host = NULL;
    while (*end && *end != '"') {
      if (*end == ' ') {
        host = ++end;
      } else {
        ++end;
      }
    }
    if (*end == '"') {
      *end = '\000';
    } else {
      // Couldn't find the closing '"'
      goto retry;
    }
    if (!host || strcmp(host, hostname()) ||
        !kill(atoi(ptr), 0) || errno != ESRCH) {
      // The lock file is either owned by a process on a different host, or
      // by a process that is still alive. We just have to wait for the lock
      // to become available.
      goto retry;
    }
    // This is a stale lock file that we can safely delete. There is a minor
    // risk of a race condition if another process clears the lock at the
    // same time. Not all that much that we can do. Hopefully, randomized
    // delays minimize this particular risk.
    // Fortunately, stale lock files should be rare anyway.
    unlink(lockFname);
    goto retry;
  }
  unlink(tmpFname);
  free(tmpFname);
  free(lockFname);
}

void unlockCfg(struct Cfg *cfg) {
  check(cfg->fp);
  char *lockFname = stringPrintf(NULL, "%s.lock", cfg->fname);
  unlink(lockFname);
  free(lockFname);
  fclose(cfg->fp);
  cfg->fp       = NULL;
}

struct JSON *lockCfgAndGetJSON(struct Cfg *cfg) {
 retry:
  lockCfg(cfg);
  struct stat sb;
  if (!stat(cfg->fname, &sb) &&
      S_ISREG(sb.st_mode) &&
      sb.st_mtime != cfg->mtime) {
    // If we have never read the configuration file, or if it has changed
    // since the last time that we read it, update our in-memory
    // representation.
    int fd = TEMP_FAILURE_RETRY(open(cfg->fname, O_RDONLY));
    if (fd >= 0) {
      char *buf;
      check(buf = malloc(sb.st_size + 1));
      check(TEMP_FAILURE_RETRY(read(fd, buf, sb.st_size) == sb.st_size));
      buf[sb.st_size] = '\000';
      destroyJSON(&cfg->json);
      initJSON(&cfg->json, buf);
      free(buf);
      cfg->mtime = sb.st_mtime;

      // If we are a daemon process, make sure the configuration file
      // points to us, if it doesn't already do so. N.B. We don't worry about
      // updating non-existing files.
      if (cfg->isDaemon && cfg->json.type == J_OBJECT) {
        const struct JSON *daemon =
          (const struct JSON *)getFromHashMap(&cfg->json.obj, "daemon");
        char *endptr;
        if (!daemon || daemon->type != J_STRING ||
            pid() != strtol(daemon->str, &endptr, 10) ||
            *endptr != ' ' || strcmp(hostname(), endptr + 1)) {
          // Committing the configuration releases the lock (this is a
          // result of us relying on "rename()" as a atomic way of updating
          // the configuration file.
          // Since we still need the lock, just try acquiring it again.
          commitCfg(cfg);
          goto retry;
        }
      }
    }
  }
  return &cfg->json;
}

static void writeString(FILE *fp, const char *str) {
  check(fputc('"', fp) == '"');
  while (str && *str) {
    unsigned codepoint;
    str = parseUTF8(str, &codepoint);
    switch (codepoint) {
    case '"':  check(fputs("\\\"", fp) != EOF); break;
    case '\\': check(fputs("\\\\", fp) != EOF); break;
    case '\b': check(fputs("\\b",  fp) != EOF); break;
    case '\f': check(fputs("\\f",  fp) != EOF); break;
    case '\n': check(fputs("\\n",  fp) != EOF); break;
    case '\r': check(fputs("\\r",  fp) != EOF); break;
    case '\t': check(fputs("\\t",  fp) != EOF); break;
    default:
      if (codepoint < ' ' || codepoint > 0x7F) {
        if (codepoint < 0x10000) {
          check(fprintf(fp, "\\u%04X", codepoint) == 6);
        } else {
          check(codepoint < 0x110000);
          check(fprintf(fp, "\\u%04X\\u%04X",
                        ((codepoint - 0x10000) >>   10) + 0xD800,
                        ((codepoint - 0x10000) & 0x3FF) + 0xDC00) == 12);
        }
      } else {
        check(fputc(codepoint, fp) == codepoint);
      }
    }
  }
  check(fputc('"', fp) == '"');
}

struct ObjWriteState {
  int                  empty;
  FILE                 *fp;
  struct Cfg           *cfg;
  int                  indent;
  const struct HashMap *exclude;
};

static void writeCfg(FILE *fp, struct Cfg *cfg, int indent, struct JSON *json,
                     const struct HashMap *exclude);
static int objWriteIterator(void *arg, const char *key, char **value) {
  struct ObjWriteState *state = (struct ObjWriteState *)arg;
  if ((state->indent > 2 ||
       (strcmp(key, "lock") && strcmp(key, "daemon"))) &&
      (!state->exclude || !getFromHashMap(state->exclude, key))) {
    check(fputs(",\n" + state->empty, state->fp) != EOF);
    state->empty = 0;
    for (int i = 0; i < state->indent; ++i) {
      check(fputc(' ', state->fp) == ' ');
    }
    writeString(state->fp, key);
    check(fputs(": ", state->fp) != EOF);
    writeCfg(state->fp, state->cfg, state->indent,
             (struct JSON *)*value, state->exclude);
  }
  return 1;
}

static void writeCfg(FILE *fp, struct Cfg *cfg, int indent, struct JSON *json,
                     const struct HashMap *exclude) {
  switch (json->type) {
  case J_STRING:
    writeString(fp, json->str);
    break;
  case J_NUMBER:
    check(fprintf(fp, "%.16g", json->number) > 0);
    break;
  case J_ARRAY:
    check(fputs("[\n", fp) != EOF);
    for (int i = 0;; ++i) {
      for (int j = 0; j < indent; ++j) {
        check(fputc(' ', fp) == ' ');
      }
      if (i < json->array_size) {
        check(fputs("  ", fp) != EOF);
        writeCfg(fp, cfg, indent+2, json->array + i, exclude);
        check(fputs(",\n" + (++i == json->array_size), fp) != EOF);
      } else {
        check(fputc(']', fp) == ']');
        break;
      }
    }
    break;
  case J_OBJECT:
    check(fputc('{', fp) == '{');
    int empty = 1;
    if (!indent) {
      if (!exclude || !getFromHashMap(exclude, "lock")) {
        // In order to make detection of stale lock files work efficiently, we
        // want the owner of the lock file to be listed in the first few
        // characters.
        check(fprintf(fp, "\n  \"lock\": \"%d %s\"",
                      (int)pid(), hostname()) > 0);
        empty = 0;
      }

      // And while we are at it, we also list the currently running daemon
      // process. This is helpful, if we need to inform the daemon that the
      // configuration file has changed.
      if (cfg->isDaemon) {
        // If we are a daemon process, update the "daemon" entry to point to
        // our process.
        char *daemon = stringPrintf(NULL, "\"%d %s\"", pid(), hostname());
        updateJSONValue(&cfg->json, "daemon", daemon);
        free(daemon);
      }

      if (!exclude || !getFromHashMap(exclude, "daemon")) {
        // Write the new "daemon" entry, or (if we are not a daemon process)
        // keep any existing old "daemon" entries.
        const struct JSON *daemon =
                 (const struct JSON *)getFromHashMap(&cfg->json.obj, "daemon");
        if (daemon && daemon->type == J_STRING) {
          check(fputs(",\n  \"daemon\": ", fp) != EOF);
          writeString(fp, daemon->str);
          empty = 0;
        }
      }
    }
    iterateOverHashMap(&json->obj, objWriteIterator, &(struct ObjWriteState){
                         .empty   = empty,
                         .fp      = fp,
                         .cfg     = cfg,
                         .indent  = indent+2,
                         .exclude = exclude });
    check(fputc('\n', fp) == '\n');
    for (int i = 0; i < indent; ++i) {
      check(fputc(' ', fp) == ' ');
    }
    check(fputc('}', fp) == '}');
    break;
  case J_FALSE:
    check(fputs("false", fp) != EOF);
    break;
  case J_TRUE:
    check(fputs("true", fp) != EOF);
    break;
  case J_NULL:
    check(fputs("null", fp) != EOF);
    break;
  }
}

static void commitShadowCfg(struct Cfg *cfg) {
  // Some embedded devices have filesystems that are very expensive to write
  // to. For these devices, we keep two configuration files. The standard
  // configuration file contains all data, gets modified semi-regularly,
  // but is stored in emphemeral storage (e.g. /tmp). It won't survive a
  // reboot of the device.
  // The shadow configuration file only contains data that cannot be recomputed
  // (e.g. no process ids, no printer status, ...). It typically will only
  // need to be written whenever a configuration change is made by the user.
  // This file lives in permanent storage. The system start up script can
  // use it after a reboot to boot strap the Cloudprint proxy.
  const struct JSON *shadow =
               (const struct JSON *)getFromHashMap(&cfg->json.obj, "shadow");
  if (shadow && shadow->type == J_STRING) {
    struct HashMap exclude;
    initHashMap(&exclude, NULL, NULL);
    addToHashMap(&exclude, "lock",   "lock");
    addToHashMap(&exclude, "daemon", "daemon");
    const struct JSON *excl =
              (const struct JSON *)getFromHashMap(&cfg->json.obj, "exclude");
    if (excl && excl->type == J_ARRAY) {
      for (int i = 0; i < excl->array_size; ++i) {
        if (excl->array[i].type == J_STRING) {
          addToHashMap(&exclude, excl->array[i].str,
                       excl->array[i].str);
        }
      }
    }
    int fd = TEMP_FAILURE_RETRY(open(shadow->str,
                                     O_RDONLY|O_NOFOLLOW|O_NOATIME));
    if (fd >= 0) {
      struct stat sb;
      if (!fstat(fd, &sb) && S_ISREG(sb.st_mode)) {
        char *buf;
        check(buf = malloc(sb.st_size + 1));
        check(TEMP_FAILURE_RETRY(read(fd, buf, sb.st_size) == sb.st_size));
        TEMP_FAILURE_RETRY(close(fd));
        buf[sb.st_size] = '\000';
        struct JSON oldShadowFile;
        initJSON(&oldShadowFile, buf);
        free(buf);
        if (isEqualJSON(&cfg->json, &oldShadowFile, &exclude)) {
          // The shadow file already contains identical data to what we store
          // in our configuration file (minus the excluded items). No need to
          // write it again.
          destroyJSON(&oldShadowFile);
          goto no_shadow_update;
        }
        destroyJSON(&oldShadowFile);
      }
      TEMP_FAILURE_RETRY(close(fd));
    }
    char *fname = stringPrintf(NULL, "%s~", shadow->str);
    check((fd = TEMP_FAILURE_RETRY(open(fname,
                                        O_WRONLY|O_CREAT|O_NOFOLLOW|O_TRUNC,
                                        0600))) >= 0);
    FILE *fp = fdopen(fd, "w");
    writeCfg(fp, cfg, 0, &cfg->json, &exclude);
    check(fputc('\n', fp) == '\n');
    fclose(fp);
    check(!rename(fname, shadow->str));
    unlink(fname);
    free(fname);
 no_shadow_update:
    destroyHashMap(&exclude);
  }
}

void commitCfg(struct Cfg *cfg) {
  if (!cfg->fp) {
    lockCfg(cfg);
  }
  check(cfg->fp);
  fseek(cfg->fp, 0, SEEK_SET);
  writeCfg(cfg->fp, cfg, 0, &cfg->json, NULL);
  check(fputc('\n', cfg->fp) == '\n');
  fflush(cfg->fp);
  struct stat sb;
  if (!fstat(fileno(cfg->fp), &sb)) {
    cfg->mtime  = sb.st_mtime;
  }
  commitShadowCfg(cfg);
  char *lockFname = stringPrintf(NULL, "%s.lock", cfg->fname);
  check(!rename(lockFname, cfg->fname));
  free(lockFname);
  fclose(cfg->fp);
  cfg->fp       = NULL;
}
